<template>
<div ref="refSelf" class="column" style="overflow: hidden; background-color: #fff; position:absolute; width:100%; height:100%; box-sizing:border-box;padding:30px;left: 0px;top: 0px;">
    <div class="row">
        <h1 class="grow noselect" style="color: #bd0000">工厂模式</h1>
        <n-button @click="bus.emit('topview.enter()','index')" title="返回">返回</n-button>
    </div>

    <!-- <n-alert type="warning" closable v-model:show="bShowWarning">
        该模式下的某些操作会向主机零件的一次性只读区写入数据，如果不清这些功能，请立即点击放回。
    </n-alert> -->

    <div class="row item-center gap20" style="margin:0px;">
        <!-- 左侧：设置区 -->
        <div style="flex:1; display:flex; flex-direction:column; gap:10px;">
            <!-- 固件列表+测试命令区域 -->
            <n-card size="small" :bordered="true" style="padding: 10px 12px 8px 12px;">
                <n-text strong>固件列表：</n-text>
                <n-space vertical size="small" style="margin-top: 8px;">
                    <div v-for="(fw, idx) in lstFirmwareFiles" :key="idx" class="row gap5 item-center">
                        <n-checkbox v-model:checked="fw.burn" />
                        <n-input v-model:value="fw.path" placeholder="固件路径" class="grow" />
                        <n-button @click="onBrowse(idx)">
                            <template #icon>
                                <svg width="16" height="16" viewBox="0 0 16 16"><path fill="currentColor" d="M2 2h12v12H2z" fill-opacity=".01"/><path fill="currentColor" d="M4 4h8v2H4zm0 4h8v2H4zm0 4h5v2H4z"/></svg>
                            </template>
                            浏览
                        </n-button>
                        <n-input
                            v-model:value="fw.addressStr"
                            placeholder="地址(0x或十进制)"
                            style="width:110px;"
                            @blur="onAddressBlur(idx)"
                        />
                        <n-button
                            quaternary
                            size="small"
                            style="padding:0 6px;"
                            @click="removeFirmware(idx)"
                            v-if="lstFirmwareFiles.length > 1"
                        >
                            <template #icon>
                                <svg width="14" height="14" viewBox="0 0 14 14">
                                    <path d="M3 3l8 8M11 3l-8 8" stroke="#444" stroke-width="1.5" stroke-linecap="round"/>
                                </svg>
                            </template>
                        </n-button>
                    </div>
                    <!-- 添加固件按钮，右对齐 -->
                    <div style="display:flex; justify-content:flex-end;">
                        <n-button size="small" dashed @click="addFirmware">
                            <template #icon>
                                <svg width="14" height="14" viewBox="0 0 14 14"><circle cx="7" cy="7" r="6" stroke="currentColor" stroke-width="1.2" fill="none"/><path d="M7 4v6M4 7h6" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/></svg>
                            </template>
                            添加固件
                        </n-button>
                    </div>
                    <!-- 测试命令区域，合并进来 -->
                    <div style="margin-top: 12px; border-top: 1px dashed #eee; padding-top: 10px;">
                        <div class="row item-center gap20" style="margin-bottom: 8px;">
                            <n-checkbox v-model:checked="bSendTestCommnad">烧录完毕后连接并发送测试命令</n-checkbox>
                            <n-checkbox v-model:checked="bBinaryTestCommand">16进制格式(eg: 0A1B2C3D4F5E)</n-checkbox>
                        </div>
                        <div style="margin-top: 10px;">
                            <n-input v-model:value="testCommand" placeholder="测试命令" :disabled="!bSendTestCommnad" />
                        </div>
                        <div class="row item-center gap20" style="margin-top: 10px;">
                            <n-checkbox v-model:checked="bWaitBeShellStartup">等待 BeShell 启动</n-checkbox>
                            <div style="display: flex; align-items: center;">
                                <span style="margin-right: 6px; color: #333;">等待超时</span>
                                <n-input-number v-model:value="beShellTimeout" :min="1" :max="120" style="width: 80px;" />
                                <span style="margin-left: 6px; color: #888;">秒</span>
                            </div>
                        </div>
                    </div>
                </n-space>
            </n-card>
        </div>
        
        <!-- 右侧：操作区 -->
        <n-card
            style="width: 300px; padding: 18px 12px 14px 12px; box-sizing: border-box; display: flex; flex-direction: column; align-items: stretch; justify-content: flex-start;"
            :bordered="false"
        >
            <!-- 按钮和提示同一行 -->
            <div style="display: flex; align-items: center; margin-bottom: 12px;">
                <n-button
                    v-if="!bAutoBurning"
                    type="primary"
                    @click="onStartBurn"
                    :disabled="bAutoBurning"
                    size="large"
                    style=" margin-right: 8px;"
                >开始自动烧录</n-button>
                <n-button
                    v-else
                    type="error"
                    @click="onStopBurn"
                    size="large"
                    style="margin-right: 8px;"
                >停止自动烧录</n-button>
                <div style="flex:1;">
                    <n-tag
                        :type="bAutoBurning ? 'info' : 'success'"
                        size="large"
                        :bordered="false"
                        style="font-weight:bold; font-size:14px; padding: 2px 10px; width:100%; justify-content:center;"
                    >
                        <template #icon>
                            <n-icon v-if="bAutoBurning" color="#2080f0"><i class="fas fa-spinner fa-spin"></i></n-icon>
                            <n-icon v-else color="#18a058"><i class="fas fa-hourglass-start"></i></n-icon>
                        </template>
                        <span v-if="bAutoBurning" style="color:#18a058;">正在烧录</span>
                        <span v-else style="color:#888;">等待开始</span>
                    </n-tag>
                </div>
            </div>
            <!-- 统计 -->
            <div style="display: flex; flex-direction: column; gap: 0px; margin-top: 20px;">
                <div style="display: flex; justify-content: center; gap: 18px;">
                    <div style="text-align: center;">
                        <div style="font-size: 13px; color: #888;">成功</div>
                        <div style="color: green; font-size: 22px; font-weight: bold;">{{ burnSuccessNum }}</div>
                    </div>
                    <div style="text-align: center;">
                        <div style="font-size: 13px; color: #888;">失败</div>
                        <div style="color: #d03050; font-size: 22px; font-weight: bold;">{{ burnErrorNum }}</div>
                    </div>
                    <div style="text-align: center;">
                        <div style="font-size: 13px; color: #888;">设备</div>
                        <div style="font-size: 22px; font-weight: bold;">{{ Object.keys(mapDeivces).length }}</div>
                    </div>
                </div>
                <div class="row end" style="margin-top: 0px;">
                    <n-button type="link" style="color: #888; margin-right: 0px; padding-right: 0px;" @click="burnSuccessNum=0; burnErrorNum=0">统计清零</n-button>
                </div>
            </div>
        </n-card>
    </div>

    <div
        ref="refDevOutter"
        class="column gap15 grow dev-outter-wrap"
        style="overflow-y: auto; flex-wrap: wrap; flex-direction: row; align-items: flex-start;margin-top:15px"
        >
        <div
            v-for="(dev, idx) in mapDeivces"
            :key="dev.path + idx"
            class="dev-outter column"
            :style="{ width: devCardWidth + 'px' }"
        >
            <div class="dev-status-row shrink0">
                <span class="dev-path" :title="dev.path">{{ dev.displayName }}</span>
                <span class="dev-status-text" :class="`dev-status-${dev.status}`">{{ dev.statusText }}</span>
            </div>
            <pre class="grow dev-output-pre" :id="'output-'+dev.id" style="font-size:12px;white-space:pre-wrap;word-break:break-all;">{{ dev.output }}</pre>
        </div>
    </div>

</div>
</template>

<style scoped>
.dev-outter-wrap {
  display: flex;
  flex-wrap: wrap;
  gap: 15px;
  align-items: flex-start;
}
.dev-outter {
  border: 1px solid #c5d6ff;
  margin: 0;
  flex: none;
  background: #f9fbff;
  border-radius: 3px;
  box-sizing: border-box;
  padding: 10px 12px;
  height: 250px;
}
.dev-status {
  padding-top: 0px;
  margin-top: 10px;
  margin-bottom: 10px;
  margin-left: 0;
  margin-right: 0;
}
.dev-status-failed {
  background-color: #ffd6d6;
}
.dev-status-burning {
  background-color: #fff5d6;
}
.dev-status-success {
  background-color: #d6ffd6;
}
.dev-status-row {
  display: flex;
  align-items: center;
  margin-bottom: 10px;
}
.dev-path {
  flex: 1 1 0;
  min-width: 0;
  font-weight: bold;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  direction: ltr;
  text-align: left;
}
.dev-status-text {
  flex: none;
  width: 80px;
  text-align: right;
  margin-left: 8px;
  font-weight: normal;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.dev-status-waiting {
  background-color: #007bff;
}
.dev-status-burning {
  background-color: #ffc107;
}
.dev-status-success {
  background-color: #28a745;
}
.dev-status-failed {
  background-color: #dc3545;
}
.dev-output-pre {
  overflow-y: auto;
}
</style>

<script lang="ts" setup>
import { ref, onMounted, reactive, Ref, watch, watchEffect } from 'vue'
import {bus} from '@beshell/events-ts/bus'
import ChromeSerialSocket from '@beshell/conn-ts/ChromeSerialSocket';
import {openFirmwareDir, fileModifyTime, loadFirmwares, requestPartMeta,
    burnFirmware, 
    testDev as funcTestDev,
    blinkDev as funcBlinkDev,
    writeEFUES_Flash3V3, burnPartMeta as _burnPartMeta, rootdir, runPython, watchUSB} from './download'
import { sleep } from '../../base/utils'

interface IFirmwareFile {
    burn: boolean;
    path: string;
    address: number;
    addressStr?: string; // 新增，辅助输入
}
interface IDevice {
    id: number;
    displayName: string;
    path: string;
    output: string;
    status: 'waiting' | 'burning' | 'success' | 'failed' ;
    statusText: string;
    burning: boolean;
    connId: number;
    vid:string ;
    pid:string ;
}

const DevCardWidthMin = 300
const DevCardWidthMax = 400


const lstFirmwareFiles = ref<IFirmwareFile[]>([
    { burn: true, path: '', address: 0x0, addressStr: '0x0' },
])
const bSendTestCommnad = ref(false)
const bBinaryTestCommand = ref(false)
const testCommand = ref('')
const bWaitBeShellStartup = ref(false)
const beShellTimeout = ref(30)

const bAutoBurning = ref(false)
const burnSuccessNum = ref(0)
const burnErrorNum = ref(0)

const mapDeivces = ref<{[key:string]:IDevice}>({})
let devIdCounter = 0



function trimUsbPath(path:string) {
    return path.replace(/[\\\.]/g, '')
}


function addFirmware() {
    lstFirmwareFiles.value.push({ burn: true, path: '', address: 0x0, addressStr: '0x0' })
}
function removeFirmware(idx: number) {
    if (lstFirmwareFiles.value.length > 1) lstFirmwareFiles.value.splice(idx, 1)
}

function onBrowse(idx: number) {
    const input = document.createElement('input')
    input.type = 'file'
    input.accept = '.bin,.hex,.img' // 可根据需要调整支持的文件类型
    input.onchange = (event: any) => {
        const file = event.target.files[0]
        if (file) {
            // Electron/Chromium下可直接获取 file.path，否则用 file.name
            const filePath = file.path || file.name
            lstFirmwareFiles.value[idx].path = filePath
        }
    }
    input.click()

}
function onAddressBlur(idx: number) {
    const fw = lstFirmwareFiles.value[idx]
    let val = fw.addressStr?.trim() || ''
    if (val.startsWith('0x') || val.startsWith('0X')) {
        fw.address = parseInt(val, 16)
    } else {
        fw.address = parseInt(val, 10)
    }
    fw.addressStr = val // 保持原输入
}

let refreshing = false
async function refreshUsbDevices() {

    if(refreshing) return
    refreshing = true


    watchUSB((event, path, vid, pid)=>{
        console.log(event, path, vid, pid)
        // 插入设备
        if(event === '+') {
            mapDeivces.value[path] = {
                displayName: trimUsbPath(path),
                path: path,
                output: '',
                status: 'waiting',
                statusText: '等待烧录', 
                burning: false ,
                id: devIdCounter++,
                connId: -1,
                vid, pid
            }

            // 自动烧录
            if(bAutoBurning.value) {
                burnDevice(mapDeivces.value[path])
            }
        }

        // 移除设备
        else if(event === '-') {
            delete mapConnId2Dev[mapDeivces[path]?.connId]
            delete mapDeivces.value[path]
        }
    })
}


const refDevOutter = ref<HTMLElement | null>(null)
const devCardWidth = ref(DevCardWidthMin)

function calcDevCardWidth() {
  if (!refDevOutter.value) return
  const gap = 15 // 与 .dev-outter-wrap 的 gap 保持一致

  // 计算每行最多能放多少卡片（保证宽度不小于最小宽度）
  let n = Math.floor((refDevOutter.value.clientWidth + gap) / (DevCardWidthMin + gap))
  if (n < 1) n = 1

  // 计算卡片宽度，使其刚好填满容器
  let width = Math.floor((refDevOutter.value.clientWidth - gap * (n - 1)) / n)

  // 限制在最小和最大宽度之间
  if (width > DevCardWidthMax) width = DevCardWidthMax
  if (width < DevCardWidthMin) width = DevCardWidthMin

  devCardWidth.value = width
}

onMounted(() => {
  calcDevCardWidth()
  window.addEventListener('resize', calcDevCardWidth)
  
  refreshUsbDevices()
})
watch(refDevOutter, () => {
  calcDevCardWidth()
})

// 可选：在卡片数量变化时也重新计算
watch(mapDeivces, () => {
  calcDevCardWidth()
})

watchEffect(() => {
    // LocalStorage keys
    const LS_FIRMWARE_LIST = 'factory_firmware_list'
    const LS_TEST_COMMAND = 'factory_test_command'
    const LS_ENABLE_TEST_COMMAND = 'factory_enable_test_command'
    const LS_BINARY_TEST_COMMAND = 'factory_binary_test_command'
    const LS_WAIT_BESHELL_STARTUP = 'factory_wait_beshell_startup'
    const LS_BESHELL_TIMEOUT = 'factory_beshell_timeout'
  
    // 加载本地存储
    function loadFromLocalStorage() {
        // 固件列表
        const fwStr = localStorage.getItem(LS_FIRMWARE_LIST)
        if (fwStr) {
            try {
                const arr = JSON.parse(fwStr)
                if (Array.isArray(arr) && arr.length > 0) {
                    lstFirmwareFiles.value = arr
                }
            } catch {}
        }
        // 测试命令
        const cmd = localStorage.getItem(LS_TEST_COMMAND)
        if (cmd !== null) {
            testCommand.value = cmd
        }
        // 是否发送测试命令
        const enable = localStorage.getItem(LS_ENABLE_TEST_COMMAND)
        if (enable !== null) {
            bSendTestCommnad.value = enable === 'true'
        }
        // 是否发送测试命令
        const isBinary = localStorage.getItem(LS_BINARY_TEST_COMMAND)
        if (isBinary !== null) {
            bBinaryTestCommand.value = isBinary === 'true'
        }
        // 等待 BeShell 启动
        const waitBeShell = localStorage.getItem(LS_WAIT_BESHELL_STARTUP)
        if (waitBeShell !== null) {
            bWaitBeShellStartup.value = waitBeShell === 'true'
        }
        // 超时时间
        const timeout = localStorage.getItem(LS_BESHELL_TIMEOUT)
        if (timeout !== null && !isNaN(Number(timeout))) {
            beShellTimeout.value = Number(timeout)
        }
    }

    // 保存到本地存储
    function saveToLocalStorage() {
        localStorage.setItem(LS_FIRMWARE_LIST, JSON.stringify(lstFirmwareFiles.value))
        localStorage.setItem(LS_TEST_COMMAND, testCommand.value)
        localStorage.setItem(LS_ENABLE_TEST_COMMAND, String(bSendTestCommnad.value))
        localStorage.setItem(LS_BINARY_TEST_COMMAND, String(bBinaryTestCommand.value))
        localStorage.setItem(LS_WAIT_BESHELL_STARTUP, String(bWaitBeShellStartup.value))
        localStorage.setItem(LS_BESHELL_TIMEOUT, String(beShellTimeout.value))
    }
  
    // 启动时自动加载
    onMounted(() => {
        loadFromLocalStorage()
        window.addEventListener('resize', calcDevCardWidth)
    })
  
    // 变更时自动保存
    watch(
        [lstFirmwareFiles, testCommand, bSendTestCommnad, bBinaryTestCommand, bWaitBeShellStartup, beShellTimeout],
        saveToLocalStorage,
        { deep: true }
    )
})


async function burnDevice(dev:IDevice) {
    if(dev.burning) {
        return
    }
    
    
    dev.burning = true 
    dev.status = 'burning'
    dev.statusText = '正在烧录...'

    let suc = false

    try {
        
        // console.log(firmjson, flashSize, filelst)     
        let binlst: any[] = []
        for(let fw of lstFirmwareFiles.value) {
            if(!fw.burn || !fw.path) continue
            let address = NaN
            if(fw.addressStr?.trim().startsWith('0x')) {
                address = parseInt(fw.addressStr||"", 16)
            }else{
                address = parseInt(fw.addressStr||"", 10)
            }
            if( !isNaN(address) ){
                binlst.push({path: fw.path, offset: address})
            }
        }
        console.log(binlst)

        let t = Date.now()

        devOutput(dev, "\r\n\r\n开始烧录固件 ...\r\n")
        suc = await burnFirmware(trimUsbPath(dev.path), binlst, (data)=>{
            devOutput(dev, data)
        })
        // esptoolsProcess.value = (promise as any).process
        
        devOutput(dev, "完成 ["+new Date().toLocaleString()+"]\r\n")
        devOutput(dev, "\r\n烧录耗时: " + Math.round((Date.now() - t) / 1000) + " 秒\r\n")
        
        if(suc) {
            burnSuccessNum.value += 1
        }

        await sleep(100)

        dev.status = suc? 'success': 'failed'
        dev.statusText = suc? '烧录成功': '烧录失败'

        if(suc && bSendTestCommnad.value) {
            await sendTestCommnad(dev)
        }

        
    }catch(e:any){
        console.error(e)
        devOutput(dev, e.toString() + "\r\n")
        dev.status = 'failed'
    }

    dev.burning = false

    if(!suc) {
        burnErrorNum.value += 1
    }

    return suc
}

function autoScrollOutput(dev: IDevice) {
    let outputEl = document.getElementById('output-' + dev.id)
    if (outputEl) {
        outputEl.scrollTo(0, outputEl.scrollHeight + 20)
    }
}

function devOutput(dev: IDevice, data: string) {
    dev.output += data
    autoScrollOutput(dev)
}

function onStartBurn() {
    bAutoBurning.value = true

    // for (let devPath in mapDeivces.value) {
    //     let dev = mapDeivces.value[devPath]
    //     if(!dev.burning) { 
    //         burnDevice(dev)
    //     }
    // }
}
function onStopBurn() {
    bAutoBurning.value = false
}


const mapConnId2Dev: {[key:number]: IDevice} = {}
chrome.serial.onReceive.addListener((info: any) => {
    // console.log("onReceive", info)
    if(!mapConnId2Dev[info.connectionId]) {
        return
    }
    const str = new TextDecoder().decode(info.data)
    devOutput(mapConnId2Dev[info.connectionId], str)
})
chrome.serial.onReceiveError.addListener((info: any) => {
    // console.log("onReceiveError", info)
    if(!mapConnId2Dev[info.connectionId]) {
        return
    }
    devOutput(mapConnId2Dev[info.connectionId], `\r\n串口接收错误: ${info.error}\r\n`)
    delete mapConnId2Dev[info.connectionId]
})

function hexToU8Arry(str: string): Uint8Array {
    // 只保留16进制字符
    let hex = str.replace(/[^0-9a-fA-F]/g, '')
    if (hex.length % 2 !== 0) hex = '0' + hex
    const arr = []
    for (let i = 0; i < hex.length; i += 2) {
        arr.push(parseInt(hex.substr(i, 2), 16))
    }
    return new Uint8Array(arr)
}

async function sendTestCommnad(dev: IDevice) {



    devOutput(dev, "\r\n\r\n发送测试命令: " + testCommand.value + "\r\n")
    let u8cmd: Uint8Array
    if (bBinaryTestCommand.value) {
        u8cmd = hexToU8Arry(testCommand.value || "")
        console.log(u8cmd)
    } else {
        const encoder = new TextEncoder()
        u8cmd = encoder.encode(testCommand.value + '\r\n')
    }

    let connectionId: number | null = null
    let receiveHandler: ((info: any) => void) | null = null
    let receiveErrorHandler: ((info: any) => void) | null = null

    try {
        // 打开串口
        const openPromise = new Promise<{ connectionId: number }>((resolve, reject) => {
            chrome.serial.connect(dev.path, { bitrate: 115200 }, (info) => {
                if (chrome.runtime.lastError || !info) {
                    reject(chrome.runtime.lastError?.message || '串口打开失败')
                } else {
                    dev.connId = info.connectionId
                    mapConnId2Dev[info.connectionId] = dev
                    resolve(info)
                }
            })
        })
        const info = await openPromise
        connectionId = info.connectionId

        // 注册串口接收监听器，始终输出内容
        let outputBuffer = ''
        let foundReady = false
        let elapsed = 0
        const timeout = beShellTimeout.value // 秒
        const interval = 10 // 秒

        receiveHandler = (info: any) => {
            if (info.connectionId === connectionId) {
                const str = new TextDecoder().decode(info.data)
                outputBuffer += str
                devOutput(dev, str)
                if (bWaitBeShellStartup.value && outputBuffer.includes("type JavaScript code to run, or 'help' for more information")) {
                    foundReady = true
                }
            }
        }
        receiveErrorHandler = (info: any) => {
            if (info.connectionId === connectionId) {
                devOutput(dev, `\r\n串口接收错误: ${info.error}\r\n`)
            }
        }
        chrome.serial.onReceive.addListener(receiveHandler)
        chrome.serial.onReceiveError.addListener(receiveErrorHandler)

        // 是否等待 BeShell 启动
        if (bWaitBeShellStartup.value) {
            devOutput(dev, "\r\n等待 BeShell 命令就绪 ... ...\r\n")
            // 等待特定字符串或超时，期间每10秒输出倒计时
            await new Promise<void>((resolve) => {
                function check() {
                    if (foundReady) {
                        resolve()
                        return
                    }
                    if (elapsed >= timeout) {
                        devOutput(dev, `\r\n等待超时(${timeout}秒)，未检测到 BeShell 就绪\n`)
                        resolve()
                        return
                    }
                    elapsed += interval
                    devOutput(dev, `\r\n等待设备命令提示符... 剩余${timeout - elapsed}秒\r\n`)
                    setTimeout(check, interval * 1000)
                }
                setTimeout(check, interval * 1000)
            })
        }
        else {
            // 简单等待3秒
            await new Promise<void>((resolve) => setTimeout(resolve, 3000))
        }

        // 发送命令
        const sendPromise = new Promise<void>((resolve, reject) => {
            chrome.serial.send(connectionId!, u8cmd, (sendInfo) => {
                if (chrome.runtime.lastError || !sendInfo) {
                    reject(chrome.runtime.lastError?.message || '发送失败')
                } else {
                    resolve()
                }
            })
        })
        await sendPromise
        devOutput(dev, "\r\n测试命令发送完成\r\n")

        // 等待串口返回数据一段时间（如 2 秒）
        await new Promise<void>((resolve) => setTimeout(resolve, 2000))
    } catch (e: any) {
        devOutput(dev, "\r\n测试命令发送失败: " + e.toString() + "\r\n")
    } finally {
        // 移除临时监听器
        if (receiveHandler) chrome.serial.onReceive.removeListener(receiveHandler)
        if (receiveErrorHandler) chrome.serial.onReceiveError.removeListener(receiveErrorHandler)
        // 关闭串口
        if (connectionId !== null) {
            chrome.serial.disconnect(connectionId, () => {})
        }
    }
}


bus.on('factory.enter', ()=>{
    console.log('factory.enter')
    calcDevCardWidth()
})

bus.on('factory.leave', (dev:IDevice)=>{
    console.log('factory.leave')
    bAutoBurning.value = false
})
</script>